package mmc.server;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import mmc.EngineObject;
import mmc.MovableEngineObject;
import mmc.Settings;
import mmc.action.buff.Buff;
import mmc.action.buff.Periodic;
import mmc.action.skill.Skill;
import mmc.cell.ServerCell;
import mmc.networking.EndCast;
import mmc.networking.Ping;
import mmc.networking.Respawn;
import mmc.networking.StartCast;
import mmc.networking.UpdateMessage;
import mmc.networking.data.AddUpdate;
import mmc.networking.data.ChunkUnload;
import mmc.networking.data.ChunkUpdate;
import mmc.networking.data.ObjectUpdate;
import mmc.networking.data.RemoveUpdate;
import mmc.stats.StandardStats;

@SuppressWarnings("serial")
public class Player extends MovableEngineObject implements Runnable{
	private String name;
	public float pitch, yaw;
	public transient float regeneration = 3f;
	
	/**
	 * Objects known by the client
	 */
	private transient List<EngineObject> knownObjects = new ArrayList<EngineObject>();
	
	/**
	 * Objects in viewdistance of player
	 */
	private transient List<EngineObject> inRange;
	
	/**
	 * Objects in range marked as dirty
	 */
	private transient Set<EngineObject> dirtyObjects = java.util.Collections.synchronizedSet(new HashSet<EngineObject>());
	
	private transient Set<ServerCell> knownChunks = new TreeSet<ServerCell>();
	private transient Set<ServerCell> dirtyChunks = new TreeSet<ServerCell>();
	private transient boolean chunkSyncInProgress = false;
	private transient RemoteClient client;
	private transient boolean running = false;
	private transient volatile boolean busy = false;
	private transient volatile long lastClientUpdateNanos=System.nanoTime();
	public transient float viewDistance = Settings.PLAYER_DEFAULT_VIEWDISTANCE;
	private transient int permittedChunkUpdates = Settings.CLIENT_INITIAL_CHUNKUPDATE_RATE;
	public transient double currentPing = 0;
	
	public transient Map<String, Float> cooldown = new HashMap<String, Float>();
	public transient Map<Buff, Float> buffs = new HashMap<Buff, Float>();
	
	public transient Skill currentCast = null;
	
	Comparator<ServerCell> playerDistanceOrder = new Comparator<ServerCell>(){
		public float distanceTo(ServerCell o1){
			float dx = x-o1.pos.x,
				dy = y-o1.pos.y,
				dz = z-o1.pos.z;
			
			return dx*dx+dy*dy+dz*dz;
		}
		
		@Override
		public int compare(ServerCell o1, ServerCell o2) {
			if(o1.equals(o2))
				return 0;
			
			float d = distanceTo(o1)-distanceTo(o2);
			
			if(Math.abs(d)<1)
				d = Math.signum(d);
			
			if(((int)d)==0)
				return 1;
			
			return (int)d;
		}
	};
	
	public Player(RemoteClient client) {
		super(0, 0, 0);
		
		this.client = client;
		name = client.toString();
		client.setPlayer(this);
		client.getServer().getScene().addObject(this);
	}

	@Override
	public void render(EngineObject observer) {
		throw new RuntimeException("engine part cannot be rendered");
	}

	@Override
	public void update(float dt) {		
		
	}
	
	@Override
	public void update(Server server, float dt){
		regeneration -= dt;
		
		if(regeneration <= 0){
			
			this.heal(2);
			this.addEnergy(10);
						
			regeneration = 3f;
		}
	}

	public String toString(){
		return String.format("%s: %.1f %.1f %.1f", name, x, y, z);
	}

	public RemoteClient getClient() {
		return client;
	}
	
	public boolean wantsUpdate(){
		return (lastClientUpdateNanos+Settings.CLIENT_UPDATE_NANOS < System.nanoTime()) && !busy && !client.isBusy();
	}

	public void start(){
		if(running == true)
			throw new RuntimeException("Player started twice!");
		
		running = true;
		new Thread(this).start();
	}
	
	public void stop(){
		running = false;
		synchronized (this) {
			this.notifyAll();
		}
	}
	
	long lastPing = System.nanoTime()+Settings.CLIENT_PING_NANOS;
	@Override
	public void run() {
		long lastUpdate = System.nanoTime()-1;
		long now;
		float dt;
		
		while(running){
			try{
				now = System.nanoTime();
				dt = (float)((now - lastUpdate)*1e-9);
				
				if(lastPing+Settings.CLIENT_PING_NANOS < System.nanoTime()){
					lastPing = System.nanoTime();
					client.post(new Ping());
				}
				
				busy = true;
				UpdateMessage m = new UpdateMessage();
				lastClientUpdateNanos = System.nanoTime();
				
				//sync objects
				if(inRange!=null){
					//send known objects flagged dirty
					synchronized (dirtyObjects) {
						for(EngineObject o : dirtyObjects){
							if(knownObjects.contains(o))
								m.addUpdate(new ObjectUpdate(o));
						}
						dirtyObjects.clear();
					}
					
					//send objects in range currently unknown
					for(EngineObject o : inRange){
						if(!knownObjects.contains(o)){
							m.addUpdate(new AddUpdate(o));
							knownObjects.add(o);
						}
					}
					
					//remove known objects too far away
					List<EngineObject> toRemove = new ArrayList<EngineObject>();
					for(EngineObject o : knownObjects){
						if(!inRange.contains(o)){
							m.addUpdate(new RemoveUpdate(o));
							toRemove.add(o);
						}
					}
					knownObjects.removeAll(toRemove);
				}
				
				//cast
				if(currentCast != null){
					currentCast.start += dt;
					if(currentCast.start >= currentCast.casttime )
						endCast();
				}
				
				
				//cooldown		
				if(!cooldown.isEmpty()){
					Map<String, Float> cc_copy = new HashMap<String, Float>();
					cc_copy.putAll(cooldown);
					
					synchronized (cc_copy) {
						for(Map.Entry<String, Float> entry : cc_copy.entrySet()){
							float help = entry.getValue()-dt;
							if(help < 0){
								cooldown.remove(entry.getKey());
							}
							else
								cooldown.put(entry.getKey(), help);			
						}
					}
				}
				
				//buffs
				if(!buffs.isEmpty()){
					Map<Buff, Float> bb_copy = new HashMap<Buff, Float>();
					bb_copy.putAll(buffs);
					
					synchronized (bb_copy) {
						for(Map.Entry<Buff, Float> entry : bb_copy.entrySet()){
							float help = entry.getValue()-dt;
							if(help < 0){
								buffs.remove(entry.getKey());
							}
							else{
								buffs.put(entry.getKey(), help);
								
								if(entry.getKey() instanceof Periodic){
									((Periodic)entry.getKey()).check(this.getClient(), dt);
								}
							}
						}
					}
				}
				
				inRange = null;
				m.pack();
				if(m.updates.length!=0)
					client.post(m);
				m = new UpdateMessage();
				
				//sync chunks
				synchronized (dirtyChunks){
					List<ServerCell> fixed = new ArrayList<ServerCell>();
					int i = 0;
					
					//unload
					if(lastClientUpdateNanos>noUnloadsUntil){
						for(ServerCell chunk: dirtyChunks){
							if(!chunk.isContainingBlockInRange(this, viewDistance*1.2f)){
								if(knownChunks.remove(chunk)){
									m.addUpdate(new ChunkUnload(chunk.pos));
								}
								fixed.add(chunk);
							}
						}
						dirtyChunks.removeAll(fixed);
						//fixed.clear();
					}
					
					Set<ServerCell> newDirty = new TreeSet<ServerCell>(playerDistanceOrder);
					newDirty.addAll(dirtyChunks);
					
					fixed = new ArrayList<ServerCell>();
					
					//load
					for(ServerCell chunk: newDirty){
						if(i>=permittedChunkUpdates){
							//interrupt chunksynch if next clientupdate should take place already
							break;
						}
						
						if(!chunk.isCompressed())
							i++;
						
						m.addUpdate(new ChunkUpdate(chunk));
						knownChunks.add(chunk);
						fixed.add(chunk);
					}
					
					chunkSyncInProgress = i == permittedChunkUpdates;
					
					if(Settings.CLIENT_TRACE_CHUNK_TRANSMISSION&&i>0){
						System.out.println("Sending "+fixed.size()+" ("+i+" uncompressed - "+permittedChunkUpdates+" permitted) chunks to "+name);
					}
					
					dirtyChunks.removeAll(fixed);
				}
				
				m.pack();
				if(m.updates.length!=0)
					client.post(m);
				busy = false;
				
				//search for new chunks in viewrange
				flagUnknownCellsInRangeDirty();
				
				if(chunkSyncInProgress){ //adjust chunkupdate-bandwith according to available bandwith (keep updaterate)
					if(lastClientUpdateNanos+Settings.CLIENT_UPDATE_NANOS > System.nanoTime()){
						permittedChunkUpdates*=2;
					}else{
						permittedChunkUpdates = Math.max(1, permittedChunkUpdates/4);
					}
					if(Settings.CLIENT_TRACE_CHUNK_TRANSMISSION)
						System.out.println("permitted chunkupdates changed to "+permittedChunkUpdates);
				}
				synchronized(this){
					try {
						this.wait();
					} catch (InterruptedException e) {}
				}		
				lastUpdate = now;	
			}catch(Exception e){
				e.printStackTrace();
				Server.instance.broadcast("Server ERROR: "+e.getMessage());
			}
		}
	}
	
	long lastSweep = System.nanoTime();
	long noUnloadsUntil=System.nanoTime()+Settings.PLAYER_SPAWN_CHUNK_PROTECTION_TIME;
	private void flagUnknownCellsInRangeDirty(){
		if(!chunkSyncInProgress) 
			if(System.nanoTime()-Settings.CLIENT_CELL_SWEEP_NANOS > lastSweep){
				lastSweep = System.nanoTime();
				
				synchronized (dirtyChunks){

					//flag unknown chunks in range
					Server.instance.getScene().getCellManager().addCellsInRange(this, viewDistance, knownChunks, dirtyChunks);
					
					//flag known chunks out of range
					if(System.nanoTime()>noUnloadsUntil)
						for(ServerCell c : knownChunks)
							if(!c.isContainingBlockInRange(this, viewDistance*1.2f)){
								dirtyChunks.add(c);
							}
				}
			}
	}
	
	public void markDirty(EngineObject o){
		dirtyObjects.add(o);
	}
	
	public void notifySendUpdate(List<EngineObject> inRange){
		this.inRange = inRange;
		
		if(busy)
			throw new RuntimeException("should not be busy here...");
			
		synchronized (this) {
			this.notifyAll();
		}
	}
	
	public void notifyChunksDirty(Set<ServerCell> chunks){
		synchronized (dirtyChunks) {
			for(ServerCell chunk : chunks)
				if(knownChunks.contains(chunk))
					dirtyChunks.add(chunk);
			}
	}

	public void setName(String name) {
		client.getServer().broadcast(this+" changed name to "+name);
		this.name = name;
	}

	public String getName() {
		return name;
	}

	@Override
	public void kill() {
		client.getServer().broadcast(getName()+" died");
		respawn(Server.instance.getScene().getSpawnPoint());
	}
	
	public void addKnownChunks(ServerCell[] chunks){
		noUnloadsUntil = System.nanoTime()+Settings.PLAYER_SPAWN_CHUNK_PROTECTION_TIME;
		
		synchronized (knownChunks) {
			for(ServerCell cell : chunks){
				if(!knownChunks.contains(cell))
					dirtyChunks.add(cell);
			}
		}
	}
	
	public void respawn(EngineObject spawnPoint){
		moveTo(spawnPoint);
		vx = vy = vz = 0;
		health = 1;
		dirty = true;
		stats.modifyActualValue(StandardStats.HP, 100, 100);
		
		Respawn r = new Respawn(client.getServer().getScene(),spawnPoint);
		addKnownChunks(r.ca);
		client.post(r);
	}
	
	public boolean knowsAbout(EngineObject o){
		return knownObjects.contains(o);
	}
	
	public boolean isObjectInRange(EngineObject o){
		return inRange.contains(o);
	}
	
	public void startCast(Skill currentCast)
	{
		this.currentCast = currentCast; 
		currentCast.start = 0f;	
		client.post(new StartCast(currentCast.getClass().getName()));
	}
	
	public void interruptCast()
	{
		currentCast = null;
	}
	
	public void endCast()
	{
		this.addEnergy(-currentCast.energy);
		dirty = true;
		
		currentCast.endCast(this.getClient());
		if(currentCast.cooldown > 0)	//cooldown?!?!
			cooldown.put(currentCast.getClass().getName(), currentCast.cooldown);
		
		
		if(currentCast instanceof Buff){
			buffs.put((Buff) currentCast, ((Buff) currentCast).duration);
		}
		
		currentCast = null;
		client.post(new EndCast(true));
	}
}
